package society.framework.core;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

import society.framework.core.communication.Message;
import society.framework.core.communication.MessageRouter;
import society.framework.core.xml.XMLAutomaton;

/**
 * Automaton that manages the automata transfers between the local application and the remote application.
 * @author Micu Andrei
 *
 */
public class AutomataTransferAutomaton extends Automaton {
	protected static final String CONNECTION_STATE = "CONNECTION_STATE";
	protected static final String IDLE_STATE = "IDLE_STATE";
	protected static final String SEND_AUTOMATON_STATE = "SEND_AUTOMATON_STATE";
	protected static final String PROCESS_AUTOMATON_REQUEST = "PROCESS_AUTOMATON_REQUEST";
	
	protected SocietyManager societyManager;
	
	protected Socket socket;
	protected DataInputStream inputStream;
	protected DataOutputStream outputStream;
	
	protected Thread pullingThread;
	protected boolean pullRunning;
	protected Thread pushingThread;
	protected boolean pushRunning;
	
	protected MessageList automatonsToSend;
	
	protected enum Errors { PULL_ERROR, PUSH_ERROR };
	
	public AutomataTransferAutomaton(final SocietyManager societyManager) {
		this.societyManager = societyManager;
		this.currentStateName = CONNECTION_STATE;
		this.automatonsToSend = new MessageList();
		this.pullRunning = false;
		this.pushRunning = false;
		
		
		/* CONNECTION_STATE:  Automaton tries to connect to the server*/
		states.put(CONNECTION_STATE, new State() {
			
			@Override
			public void stateCode() {
				for(String address : SocietyInformation.getRemoteSocietyAddresses()){
					try {
						if(socket!=null){
							automatonsToSend.cancelRemoving();
							socket.close();
						}
						MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
				                new Message("AutomataTransferAutomaton",
				                		"Connecting to "+address+":"+ SocietyInformation.getAutomataTransferPort() 
				                		+" for automata transfer."));
						socket = new Socket();
						socket.connect(new InetSocketAddress(address, SocietyInformation.getAutomataTransferPort()),
								SocietyInformation.RECONNECTION_TIMEOUT_FOR_TRANSFERS);
						if(socket.isConnected()){
							inputStream = new DataInputStream(socket.getInputStream());
							outputStream = new DataOutputStream(socket.getOutputStream());

							pullRunning = true;
							pushRunning = true;
							
							pullingThread = new Thread(){

								@Override
								public void run() {
									super.run();
									pullAutomatons();
								}
								
							};
							pushingThread = new Thread(){

								@Override
								public void run() {
									super.run();
									pushAutomatons();
								}
								
							};
							
							pullingThread.setName("AutomataTransferPullingThread");
							pushingThread.setName("AutomataTransferPushingThread");
							
							pullingThread.start();
							pushingThread.start();
							
							MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
					                new Message("AutomataTransferAutomaton",
					                		"green:Connected successfully to "+address+":"+ SocietyInformation.getAutomataTransferPort() 
					                		+" for automata transfer."));
							break;
						} else {
							MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
					                new Message("AutomataTransferAutomaton",
					                		"red:Failed to connect to "+address+":"+ SocietyInformation.getAutomataTransferPort() 
					                		+" for automata transfer."));
						}
					} catch (Exception e) {
						socket = null;
						MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
				                new Message("AutomataTransferAutomaton",
				                		"red:Failed to connect to "+address+":"+ SocietyInformation.getAutomataTransferPort() 
				                		+" for automata transfer."));
					}
				}
			}
			
		});
		
		states.get(CONNECTION_STATE).transition = new Transition(){

			@Override
			public String transitionCode() {
				if(socket!=null){
					return IDLE_STATE;
				} else {
					return CONNECTION_STATE;
				}
			}
			
		};
		
		
		/* IDLE_STATE: Automaton waits for a message from the SocietyManager or from the threads. */
		
		states.put(IDLE_STATE, new NullState());
		
		states.get(IDLE_STATE).transition = new Transition(){

			@Override
			public String transitionCode() {
				getNextMessage();
				if(currentMessage.from.equals("AutomataTransferAutomaton")){
					if(currentMessage.data.equals(Errors.PULL_ERROR) || currentMessage.data.equals(Errors.PUSH_ERROR)){
						return CONNECTION_STATE;
					} else {
						return PROCESS_AUTOMATON_REQUEST;
					}
				} else {
					return SEND_AUTOMATON_STATE;
				}
			}
			
		};
		
		
		/* SEND_AUTOMATON_STATE: Automaton adds the automaton to the list of automatons to send. */
		
		states.put(SEND_AUTOMATON_STATE, new State(){

			@Override
			public void stateCode() {
				automatonsToSend.add(currentMessage);
			}
			
		});
		
		states.get(SEND_AUTOMATON_STATE).transition = new SimpleTransition(IDLE_STATE);
		
		
		/* PROCESS_AUTOMATON_REQUEST: Responds to a request from the remote application by sending the specified automaton. */
		
		states.put(PROCESS_AUTOMATON_REQUEST, new State(){

			@Override
			public void stateCode() {
				societyManager.sendXMLAutomatonToRemoteApplication(currentMessage.data.toString());
			}
			
		});
		
		states.get(PROCESS_AUTOMATON_REQUEST).transition = new SimpleTransition(IDLE_STATE);
	}
	
	protected void pullAutomatons(){
		try {
			XMLAutomaton automaton;
			while(pullRunning){
				automaton = new XMLAutomaton();
				if(automaton.deserializeFromStream(inputStream)){
					societyManager.addAutomaton(automaton.getName(), automaton);
					automaton.start();
					
					MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
			                new Message("AutomataTransferAutomaton",
			                		"green:Received automaton: " + automaton.getName() + "."));
				} else {
					throw new Exception();
				}
			}
		} catch (Exception e) {
			AutomataTransferAutomaton.this.addMessage(new Message("AutomataTransferAutomaton", Errors.PULL_ERROR));
			MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
	                new Message("AutomataTransferAutomaton",
	                		"red:An error occured while pulling an automaton."));
		}
	}
	
	protected void pushAutomatons(){
		try {
			Message message;
			XMLAutomaton automaton;
			while(pushRunning){
				message = automatonsToSend.remove();
				if(message == null){
					return;
				}
				automaton = (XMLAutomaton) message.data;
				automaton.restoreSnapshot();
				if(automaton.serializeToStream(outputStream)){
					outputStream.flush();
	
					societyManager.removeAutomaton(automaton.getName());
					MessageRouter.getInstance().unsubscribe(automaton.getName());
					
					MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
			                new Message("AutomataTransferAutomaton",
			                		"green:Sent automaton: " + automaton.getName() + "."));
				} else {
					MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
			                new Message("AutomataTransferAutomaton",
			                		"red:An error occured while serializing " + automaton.getName()));
				}
			}
		} catch (Exception e) {
			AutomataTransferAutomaton.this.addMessage(new Message("AutomataTransferAutomaton", Errors.PUSH_ERROR));
			MessageRouter.getInstance().sendLocal(SocietyInformation.UI_MANAGER_NAME,
	                new Message("AutomataTransferAutomaton",
	                		"red:An error occured while pushing an automaton."));
		}
	}
}
